// (c) Copyright slimCODE Software Inc. - www.slimcode.com
// This source is subject to the Microsoft Public License (Ms-PL).
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
// All other rights reserved.

using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using System.Text;

namespace SlimCode.Utils
{
  internal class AuthenticationStream : Stream
  {
    public AuthenticationStream( Stream innerStream, byte[] authenticationKey, AuthenticationType authenticationType, bool writing )
    {
      if( innerStream == null )
        throw new ArgumentNullException( "innerStream" );

      if( writing && !innerStream.CanWrite )
        throw new ArgumentException( "Cannot write to inner stream.", "innerStream" );

      if( !writing && !innerStream.CanRead )
        throw new ArgumentException( "Cannot read from inner stream.", "innerStream" );

      if( authenticationKey == null )
        throw new ArgumentNullException( "authenticationKey" );

      if( authenticationKey.Length < 16 )
        throw new ArgumentException( "The authentication key cannot be less than 16 bytes." );

      switch( authenticationType )
      {
#if !PocketPC
        case AuthenticationType.MD5:
          m_hmac = new HMACMD5( authenticationKey );
          break;

        case AuthenticationType.SHA1:
          m_hmac = new HMACSHA1( authenticationKey, true );
          break;

        case AuthenticationType.SHA256:
          m_hmac = new HMACSHA256( authenticationKey );
          break;

        case AuthenticationType.SHA384:
          m_hmac = new HMACSHA256( authenticationKey );
          break;
#endif
        case AuthenticationType.SHA512:
          m_hmac = new HMACSHA512( authenticationKey );
          break;

        default:
          throw new ArgumentException( "Unknown authentication type.", "authenticationType" );
      }

      m_innerStream = innerStream;
      m_writing = writing;
    }

    public override bool CanRead
    {
      get { return !m_writing; }
    }

    public override bool CanSeek
    {
      get { return false; }
    }

    public override bool CanWrite
    {
      get { return m_writing; }
    }

    public override void Flush()
    {
      m_innerStream.Flush();
    }

    public override long Length
    {
      get { return m_innerStream.Length; }
    }

    public override long Position
    {
      get { return m_innerStream.Position; }
      set { throw new NotSupportedException( "This stream does not support seeking." ); }
    }

    public override int Read( byte[] buffer, int offset, int count )
    {
      if( m_writing )
        throw new IOException( "Cannot read from this stream. It is open for writing." );

      if( buffer == null )
        throw new ArgumentNullException( "buffer" );

      if( ( offset < 0 ) || ( offset >= buffer.Length ) )
        throw new ArgumentOutOfRangeException( "offset" );

      if( count < 0 )
        throw new ArgumentOutOfRangeException( "count" );

      if( offset + count > buffer.Length )
        throw new ArgumentOutOfRangeException( "count" );

      if( count == 0 )
        return 0;

      int read = 0;
      int hashSize = m_hmac.HashSize / 8;

      if( m_bufferAvailable > hashSize )
      {
        read = ( count < ( m_bufferAvailable - hashSize ) ) ? ( count ) : ( m_bufferAvailable - hashSize );

        Array.Copy( m_buffer, 0, buffer, offset, read );

        m_hmac.TransformBlock( buffer, offset, read, buffer, offset );

        offset += read;
        count -= read;
        
        m_bufferAvailable -= read;

        Array.Copy( m_buffer, read, m_buffer, 0, m_bufferAvailable );
      }

      if( count > 0 )
      {
        int innerRead = m_innerStream.Read( m_buffer, m_bufferAvailable, m_buffer.Length - m_bufferAvailable );

        if( innerRead > 0 )
        {
          m_bufferAvailable += innerRead;

          read += this.Read( buffer, offset, count );
        }
      }

      return read;
    }

    public override long Seek( long offset, SeekOrigin origin )
    {
      throw new NotSupportedException( "This stream does not support seeking." );
    }

    public override void SetLength( long value )
    {
      throw new NotSupportedException( "This stream does not support seeking." );
    }

    public override void Write( byte[] buffer, int offset, int count )
    {
      if( !m_writing )
        throw new IOException( "Cannot write to this stream. It is open for reading." );

      if( buffer == null )
        throw new ArgumentNullException( "buffer" );

      if( ( offset < 0 ) || ( offset >= buffer.Length ) )
        throw new ArgumentOutOfRangeException( "offset" );

      if( count < 0 )
        throw new ArgumentOutOfRangeException( "count" );

      if( offset + count > buffer.Length )
        throw new ArgumentOutOfRangeException( "count" );

      if( count > 0 )
      {
        m_hmac.TransformBlock( buffer, offset, count, buffer, offset );

        m_innerStream.Write( buffer, offset, count );
      }
    }

    protected override void Dispose( bool disposing )
    {
      try
      {
        if( disposing )
        {
          m_hmac.TransformFinalBlock( new byte[ 0 ], 0, 0 );

          byte[] hash = m_hmac.Hash;

          System.Diagnostics.Trace.Assert( hash.Length == ( m_hmac.HashSize / 8 ) );

          if( m_writing )
          {
            System.Diagnostics.Trace.Assert( hash.Length == ( m_hmac.HashSize / 8 ) );

            m_innerStream.Write( hash, 0, hash.Length );
          }
          else
          {
            if( hash.Length != m_bufferAvailable )
              throw new IOException( "Invalid authentication hash length." );

            for( int i = 0; i < hash.Length; i++ )
            {
              if( hash[ i ] != m_buffer[ i ] )
                throw new IOException( "Invalid authentication hash signature." );
            }
          }

          m_innerStream.Close();
          ( ( IDisposable )m_hmac ).Dispose();
        }

        m_innerStream = null;
        m_hmac = null;
      }
      finally
      {
        base.Dispose( disposing );
      }
    }

    private byte[] m_buffer = new byte[ 32768 ];
    private int m_bufferAvailable; // = 0

    private Stream m_innerStream; // = null
    private bool m_writing; // = false
    private HMAC m_hmac; // = null
  }
}
